﻿program Launcher;

uses
  Vcl.Forms,
  System.SysUtils,
  System.IniFiles,
  Winapi.ShellAPI,
  Winapi.Windows,
  IdHashMessageDigest,
  System.Classes,
  System.IOUtils;

const
  defaultLauncherIniFileName = 'Launcher.ini';
  defaultBinaryFileName = 'SPlayer.exe';
  defaultProductVersion = '1.0.0.0';
  defaultIniCommonSectionName = 'General';
  defaultLauncherMutex = '{215AF894-05AE-43F4-B39B-1E066300F97B}';
  defaultProductArchitecture = 'AnyCPU';
  defaultProductMinOSVersion = 5.1;

var
  launcherMutex : string;
  currentVersion : string;
  removeOutdatedVersions : boolean;
  splayerPath : string;
  splayerExe : string;
  iniFilePath : string;
  preserveVersion : string;
  MD5 : string;
  arch : string;
  minOSVersion : double;
  dir : string;
  validation : boolean;
  params : string;
  splayerFullPath : string;
  myIniFile : TIniFile;
  tmpStr : string;

label
  LABEL_FILE_NOT_FOUND,
  LABEL_ARCH_NOT_MATCH,
  LABEL_OS_VERSION_NOT_MATCH,
  LABEL_END;

{$R *.res}

function is_installing_older_version(version_installing_now : string; version_installed_before : string) : boolean;
var
  installedVer : array[1..10] of longint;
  installingVer : array[1..10] of longint;
  oldVer, nowVer : string;
  i, oldTotal, nowTotal, total : integer;

begin
  oldTotal := 1;
  while (Pos('.', version_installed_before) > 0) do
  begin
    oldVer := version_installed_before;
    Delete(oldVer, Pos('.', oldVer), ((Length(oldVer) - Pos('.', oldVer)) + 1));
    installedVer[oldTotal] := StrToIntDef(oldVer, 0);
    oldTotal := oldTotal + 1;
    version_installed_before := Copy(version_installed_before, (Pos('.', version_installed_before) + 1), (Length(version_installed_before) - Pos('.', version_installed_before)));
  end;
  if Not SameText(version_installed_before, EmptyStr) then
  begin
    installedVer[oldTotal] := StrToIntDef(version_installed_before, 0);
  end else
  begin
    oldTotal := oldTotal - 1;
  end;
  nowTotal := 1;
  while (Pos('.', version_installing_now) > 0) do
  begin
    nowVer := version_installing_now;
    Delete(nowVer, Pos('.', nowVer), ((Length(nowVer) - Pos('.', nowVer)) + 1));
    installingVer[nowTotal] := StrToIntDef(nowVer, 0);
    nowTotal := nowTotal + 1;
    version_installing_now := Copy(version_installing_now, (Pos('.', version_installing_now) + 1), (Length(version_installing_now) - Pos('.', version_installing_now)));
  end;
  if Not SameText(version_installing_now, EmptyStr) then
  begin
    installingVer[nowTotal] := StrToIntDef(version_installing_now, 0);
  end else
  begin
    nowTotal := nowTotal - 1;
  end;
  if (oldTotal < nowTotal) then
  begin
    for i := (oldTotal + 1) to nowTotal do
    begin
      installedVer[i] := 0;
    end;
    total := nowTotal;
  end else if (oldTotal > nowTotal) then
  begin
    for i := (nowTotal + 1) to oldTotal do
    begin
      installingVer[i] := 0;
    end;
    total := oldTotal;
  end else
  begin
    total := nowTotal;
  end;
  for i := 1 to total do
  begin
    if (installedVer[i] > installingVer[i]) then
    begin
      Result := True;
      Exit;
    end else if (installedVer[i] < installingVer[i]) then
    begin
      Result := False;
      Exit;
    end else
    begin
      Continue;
    end;
  end;
  Result := False;
end;

function GetMD5(filePath : string) : string;
var
  myStr : string;
  myMemStream : TMemoryStream;
  myMD5 : TIdHashMessageDigest5;

begin
  myStr := EmptyStr;
  if Not SameText(filePath, EmptyStr) then
  begin
    if FileExists(filePath) then
    begin
      myMemStream := TMemoryStream.Create;
      myMemStream.LoadFromFile(filePath);
      myMD5 := TIdHashMessageDigest5.Create;
      myStr := UpperCase(myMD5.HashStreamAsHex(myMemStream));
      myMD5.Free;
      myMemStream.Free;
    end;
  end;
  Result := UpperCase(myStr);
end;

procedure Uninstall;
var
  mySearchRec : TSearchRec;

begin
  if (removeOutdatedVersions = True) then
  begin
    if (FindFirst(ExtractFilePath(Application.ExeName) + '*', faAnyFile, mySearchRec) = 0) then
    begin
      repeat
      begin
        Application.ProcessMessages;
        if (SameText(mySearchRec.Name, '.') or SameText(mySearchRec.Name, '..') or SameText(mySearchRec.Name, 'Licenses') or SameText(mySearchRec.Name, dir) or SameText(mySearchRec.Name, preserveVersion)) then
        begin
          Continue;
        end;
        if DirectoryExists(ExtractFilePath(Application.ExeName) + mySearchRec.Name) then
        begin
          TDirectory.Delete(ExtractFilePath(Application.ExeName) + mySearchRec.Name, True);
        end;
      end;
      until (FindNext(mySearchRec) <> 0);
      System.SysUtils.FindClose(mySearchRec);
    end;
  end;
end;

procedure RunApp(fileExe : string);
var
  fullParam : string;
  filePath : string;
  i : integer;

begin
  if (not SameText(fileExe, EmptyStr)) then
  begin
    if FileExists(fileExe) then
    begin
      filePath := ExtractFileDir(fileExe);
      fullParam := EmptyStr;
      if ((ParamCount > 0) or (not SameText(params, EmptyStr))) then
      begin
        if (ParamCount > 0) then
        begin
          for i := 1 to ParamCount do
          begin
            fullParam := fullParam + ' ' + '"' + ParamStr(i) + '"';
          end;
          fullParam := Trim(fullParam);
        end;
        if (not SameText(params, EmptyStr)) then
        begin
          fullParam := Trim(fullParam + ' ' + '"' + params + '"');
        end;
        ShellExecute(0, PWideChar('open'), PWideChar(fileExe), PWideChar(fullParam), PWideChar(filePath), 1);
      end else
      begin
        ShellExecute(0, PWideChar('open'), PWideChar(fileExe), nil, PWideChar(filePath), 1);
      end;
    end;
  end;
end;

procedure ParseLauncherParams;
var
  i : integer;

begin
  currentVersion := defaultProductVersion;
  arch := defaultProductArchitecture;
  minOSVersion := defaultProductMinOSVersion;
  dir := EmptyStr;
  splayerExe := defaultBinaryFileName;
  params := EmptyStr;
  validation := True;
  MD5 := EmptyStr;
  removeOutdatedVersions := True;
  preserveVersion := EmptyStr;
  if (ParamCount > 0) then
  begin
    for i := 1 to ParamCount do
    begin
      if SameText(ParamStr(i), '-version') then
      begin
        currentVersion := ParamStr(i + 1);
        Continue;
      end;
      if SameText(ParamStr(i), '-arch') then
      begin
        arch := ParamStr(i + 1);
        Continue;
      end;
      if SameText(ParamStr(i), '-minosver') then
      begin
        minOSVersion := StrToFloat(ParamStr(i + 1));
        Continue;
      end;
      if SameText(ParamStr(i), '-dir') then
      begin
        dir := ParamStr(i + 1);
        Continue;
      end;
      if SameText(ParamStr(i), '-bin') then
      begin
        splayerExe := ParamStr(i + 1);
        Continue;
      end;
      if SameText(ParamStr(i), '-params') then
      begin
        params := ParamStr(i + 1);
        Continue;
      end;
      if SameText(ParamStr(i), '-check') then
      begin
        if SameText(ParamStr(i + 1), 'No') then
        begin
          validation := False;
        end;
        Continue;
      end;
    end;
  end;
  if SameText(dir, EmptyStr) then
  begin
    dir := currentVersion;
  end;
  splayerPath := ExtractFilePath(Application.ExeName) + dir;
  splayerFullPath := splayerPath + '\' + splayerExe;
  if FileExists(splayerFullPath) then
  begin
    MD5 := UpperCase(GetMD5(splayerFullPath));
  end;
end;

procedure Install;
begin
  myIniFile := TIniFile.Create(iniFilePath);
  try
  begin
    if (not FileExists(iniFilePath)) then
    begin
      myIniFile.WriteBool(defaultIniCommonSectionName, 'RemoveOutdatedVersions', removeOutdatedVersions);
    end;
    myIniFile.WriteString(defaultIniCommonSectionName, 'CurrentVersion', currentVersion);
    myIniFile.WriteString(currentVersion, 'Architecture', arch);
    myIniFile.WriteFloat(currentVersion, 'MinOSVersion', minOSVersion);
    myIniFile.WriteString(currentVersion, 'Directory', dir);
    myIniFile.WriteString(currentVersion, 'Binary', splayerExe);
    myIniFile.WriteBool(currentVersion, 'Validation', validation);
    myIniFile.WriteString(currentVersion, 'MD5', UpperCase(MD5));
    myIniFile.WriteString(currentVersion, 'Parameters', params);
  end;
  finally
    myIniFile.Free;
  end;
end;

function IsWOW64 : boolean;
var
  ret : BOOL;

begin
  if (GetProcAddress(GetModuleHandle(PWideChar(kernel32)), PAnsiChar('IsWow64Process')) <> nil) then
  begin
    IsWow64Process(GetCurrentProcess, ret);
  end;
  if (ret = True) then
  begin
    Result := True;
  end else
  begin
    Result := False;
  end;
end;

function GetCurrentOSVersion : double;
var
  ver : TOSVersionInfo;

begin
  ver.dwOSVersionInfoSize := SizeOf(TOSVersionInfo);
  GetVersionEx(ver);
  Result := double(ver.dwMajorVersion + 0.1 * double(ver.dwMinorVersion));
end;

procedure RemoveOldVersion(version : string);
var
  path : string;
  fullPath : string;

begin
  if (not SameText(version, EmptyStr)) then
  begin
    if FileExists(iniFilePath) then
    begin
      myIniFile := TIniFile.Create(iniFilePath);
      path := myIniFile.ReadString(version, 'Directory', version);
      try
        myIniFile.EraseSection(version);
      finally
        myIniFile.Free;
      end;
    end else
    begin
      path := version;
    end;
    fullPath := ExtractFilePath(Application.ExeName) + path;
    if DirectoryExists(fullPath) then
    begin
      TDirectory.Delete(fullPath, True);
    end;
  end;
end;

begin
  iniFilePath := ExtractFilePath(Application.ExeName) + defaultLauncherIniFileName;
  myIniFile := TIniFile.Create(iniFilePath);
  launcherMutex := myIniFile.ReadString(defaultIniCommonSectionName, 'LauncherMutex', defaultLauncherMutex);
  tmpStr := myIniFile.ReadString(defaultIniCommonSectionName, 'CurrentVersion', '0.0.0');
  myIniFile.Free;
  CreateMutex(nil, False, PWideChar(launcherMutex));
  if (ParamCount > 0) then
  begin
    if SameText(ParamStr(1), '--install') then
    begin
      ParseLauncherParams;
      if is_installing_older_version(currentVersion, tmpStr) then
      begin
        goto LABEL_END;
      end;
      ShellExecute(0, PWideChar('open'), PWideChar(Application.ExeName), PWideChar('--uninstall ' + tmpStr), PWideChar(ExtractFileDir(Application.ExeName)), 0);
      Install;
      goto LABEL_END;
    end
    else if SameText(ParamStr(1), '--uninstall') then
    begin
      if (ParamCount >= 2) then
      begin
        RemoveOldVersion(ParamStr(2));
      end else
      begin
        dir := EmptyStr;
        preserveVersion := EmptyStr;
        removeOutdatedVersions := True;
        Uninstall;
      end;
      goto LABEL_END;
    end;
  end;
  myIniFile := TIniFile.Create(iniFilePath);
  currentVersion := myIniFile.ReadString(defaultIniCommonSectionName, 'CurrentVersion', defaultProductVersion);
  removeOutdatedVersions := myIniFile.ReadBool(defaultIniCommonSectionName, 'RemoveOutdatedVersions', True);
  if (removeOutdatedVersions = True) then
  begin
    preserveVersion := myIniFile.ReadString(defaultIniCommonSectionName, 'PreserveVersion', EmptyStr);
  end else
  begin
    preserveVersion := EmptyStr;
  end;
  arch := myIniFile.ReadString(currentVersion, 'Architecture', defaultProductArchitecture);
  minOSVersion := myIniFile.ReadFloat(currentVersion, 'MinOSVersion', defaultProductMinOSVersion);
  dir := myIniFile.ReadString(currentVersion, 'Directory', currentVersion);
  splayerPath := ExtractFilePath(Application.ExeName) + dir;
  splayerExe := myIniFile.ReadString(currentVersion, 'Binary', defaultBinaryFileName);
  splayerFullPath := splayerPath + '\' + splayerExe;
  validation := myIniFile.ReadBool(currentVersion, 'Validation', True);
  if (validation = True) then
  begin
    MD5 := UpperCase(myIniFile.ReadString(currentVersion, 'MD5', EmptyStr));
  end else
  begin
    MD5 := EmptyStr;
  end;
  params := myIniFile.ReadString(currentVersion, 'Parameters', EmptyStr);
  myIniFile.Free;
  if (validation = True) then
  begin
    if SameText(MD5, EmptyStr) then
    begin
      goto LABEL_FILE_NOT_FOUND;
    end;
  end;
  if FileExists(splayerFullPath) then
  begin
    if (validation = True) then
    begin
      if (not SameText(UpperCase(MD5), UpperCase(GetMD5(splayerFullPath)))) then
      begin
        goto LABEL_FILE_NOT_FOUND;
      end;
    end;
    if (SameText(arch, 'AnyCPU') or (SameText(arch, 'x86') and (not IsWOW64)) or (SameText(arch, 'x64') and IsWOW64)) then
    begin
      if (GetCurrentOSVersion >= minOSVersion) then
      begin
        RunApp(splayerFullPath);
        Uninstall;
        goto LABEL_END;
      end else
      begin
        goto LABEL_OS_VERSION_NOT_MATCH;
      end;
    end else
    begin
      goto LABEL_ARCH_NOT_MATCH;
    end;
  end else
  begin
    splayerPath := ExtractFileDir(Application.ExeName);
    splayerFullPath := splayerPath + '\' + defaultBinaryFileName;
    if FileExists(splayerFullPath) then
    begin
      if (validation = True) then
      begin
        if (not SameText(UpperCase(MD5), UpperCase(GetMD5(splayerFullPath)))) then
        begin
          goto LABEL_FILE_NOT_FOUND;
        end;
      end;
      if (SameText(arch, 'AnyCPU') or (SameText(arch, 'x86') and (not IsWOW64)) or (SameText(arch, 'x64') and IsWOW64)) then
      begin
        if (GetCurrentOSVersion >= minOSVersion) then
        begin
          RunApp(splayerFullPath);
          Uninstall;
          goto LABEL_END;
        end else
        begin
          goto LABEL_OS_VERSION_NOT_MATCH;
        end;
      end else
      begin
        goto LABEL_ARCH_NOT_MATCH;
      end;
    end else
    begin
      goto LABEL_FILE_NOT_FOUND;
    end;
  end;
LABEL_ARCH_NOT_MATCH:
  Application.MessageBox(PWideChar('CPU architecture not match, contact the publisher of this program for details.'), PWideChar('Architecture not match'));
  goto LABEL_END;
LABEL_OS_VERSION_NOT_MATCH:
  Application.MessageBox(PWideChar('OS version not match, contact the publisher of this program for details.'), PWideChar('OS version not match'));
  goto LABEL_END;
LABEL_FILE_NOT_FOUND:
  Application.MessageBox(PWideChar('Cannot find "SPlayer.exe", reinstall this program may solve this problem.'), PWideChar('File not found'));
LABEL_END:
  Application.Terminate;
end.
